package com.stark.commons.lang.util;

import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.*;
import lombok.experimental.Accessors;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.*;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.message.BasicNameValuePair;

import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;

/**
 * Java 发送 HTTP 请求。
 * @author Ben
 * @since 1.0.0
 * @version 1.0.0
 */
@SuppressWarnings("unused")
public class HttpUtil {
	
	private static final String CHARSET = "utf-8";
	private static final String APPLICATION_FORM_URLENCODED = "application/x-www-form-urlencoded; charset=" + CHARSET;
	
	private static final int DEFAULT_CONNECT_TIMEOUT = 1000;
	private static final int DEFAULT_READ_TIMEOUT = 3000;
	
	private static final ThreadLocal<Integer> CONNECT_TIMEOUT = new ThreadLocal<>();
	private static final ThreadLocal<Integer> READ_TIMEOUT = new ThreadLocal<>();
	
	/**
	 * 发送 get 请求。
	 * @param url 请求地址。
	 * @param headers 请求头参数集合。
	 * @param querys 地址栏请求参数集合。
	 * @return 响应对象。
	 */
	public static HttpResponse doGet(String url, Map<String, String> headers, Map<String, String> querys)
			throws Exception {
		CloseableHttpClient httpClient = wrapClient(url);
		HttpGet request = new HttpGet(buildUrl(url, querys));
		setConfig(request);
		setHeaders(request, headers);

		return httpClient.execute(request);
    }
	
	/**
	 * 发送 post 请求。
	 * <p>提交表单数据。
	 * @param url 请求地址。
	 * @param headers 请求头参数集合。
	 * @param querys 地址栏请求参数集合。
	 * @param bodys 表单参数集合。
	 * @return 响应对象。
	 */
	public static HttpResponse doPost(String url, Map<String, String> headers, Map<String, String> querys, Map<String, String> bodys)
            throws Exception {
		CloseableHttpClient httpClient = wrapClient(url);
		HttpPost request = new HttpPost(buildUrl(url, querys));
		setConfig(request);
		setHeaders(request, headers);

		if (MapUtils.isNotEmpty(bodys)) {
			List<NameValuePair> nameValuePairList = bodys.entrySet()
					.stream()
					.map(entry -> new BasicNameValuePair(entry.getKey(), entry.getValue()))
					.collect(Collectors.toList());
			UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(nameValuePairList, CHARSET);
			formEntity.setContentType(APPLICATION_FORM_URLENCODED);
			request.setEntity(formEntity);
		}

		return httpClient.execute(request);
	}
	
	/**
	 * 发送 post 请求。
	 * @param url 请求地址。
	 * @param headers 请求头参数集合。
	 * @param querys 地址栏请求参数集合。
	 * @param body 字符串类型请求体。
	 * @return 响应对象。
	 */
	public static HttpResponse doPost(String url, Map<String, String> headers, Map<String, String> querys, String body)
            throws Exception {
		CloseableHttpClient httpClient = wrapClient(url);
		HttpPost request = new HttpPost(buildUrl(url, querys));
		setConfig(request);
		setHeaders(request, headers);

		if (StringUtils.isNotBlank(body)) {
			request.setEntity(new StringEntity(body, CHARSET));
		}

		return httpClient.execute(request);
    }
	
	/**
	 * 发送 post 请求。
	 * @param url 请求地址。
	 * @param headers 请求头参数集合。
	 * @param querys 地址栏请求参数集合。
	 * @param body 文件类型请求体。
	 * @return 响应对象。
	 */
	public static HttpResponse doPost(String url, Map<String, String> headers, Map<String, String> querys, byte[] body)
            throws Exception {
		CloseableHttpClient httpClient = wrapClient(url);
		HttpPost request = new HttpPost(buildUrl(url, querys));
		setConfig(request);
		setHeaders(request, headers);

		if (ArrayUtils.isNotEmpty(body)) {
			request.setEntity(new ByteArrayEntity(body));
		}

		return httpClient.execute(request);
    }
	
	/**
	 * 发送 post 请求。
	 * <p>上传文件。
	 * @param url 请求地址。
	 * @param headers 请求头参数集合。
	 * @param querys 地址栏请求参数集合。
	 * @param bodys 表单参数集合。
	 * @param files 上传文件集合。
	 * @return 响应对象。
	 */
	public static HttpResponse doPost(String url, Map<String, String> headers, Map<String, String> querys, Map<String, String> bodys, List<ByteArrayMultipartFile> files)
            throws Exception {
		CloseableHttpClient httpClient = wrapClient(url);
		HttpPost request = new HttpPost(buildUrl(url, querys));
		setConfig(request);
		setHeaders(request, headers);

		MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create();
		multipartEntityBuilder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
		if (MapUtils.isNotEmpty(bodys)) {
			bodys.forEach(multipartEntityBuilder::addTextBody);
		}
		if (CollectionUtils.isNotEmpty(files)) {
			files.forEach(file -> multipartEntityBuilder.addBinaryBody(file.getName(), file.getBytes(), ContentType.MULTIPART_FORM_DATA, file.getOriginalFilename()));
		}
		HttpEntity reqEntity = multipartEntityBuilder.setCharset(Charset.forName(CHARSET)).build();
		request.setEntity(reqEntity);

		return httpClient.execute(request);
    }
	
	/**
	 * 发送 put 请求。
	 * <p>更新表单数据。
	 * @param url 请求地址。
	 * @param headers 请求头参数集合。
	 * @param querys 地址栏请求参数集合。
	 * @param bodys 表单参数集合。
	 * @return 响应对象。
	 */
	public static HttpResponse doPut(String url, Map<String, String> headers, Map<String, String> querys, Map<String, String> bodys)
            throws Exception {
		CloseableHttpClient httpClient = wrapClient(url);
		HttpPut request = new HttpPut(buildUrl(url, querys));
		setConfig(request);
		setHeaders(request, headers);

		if (MapUtils.isNotEmpty(bodys)) {
			List<NameValuePair> nameValuePairList = bodys.entrySet()
					.stream()
					.map(entry -> new BasicNameValuePair(entry.getKey(), entry.getValue()))
					.collect(Collectors.toList());
			UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(nameValuePairList, CHARSET);
			formEntity.setContentType(APPLICATION_FORM_URLENCODED);
			request.setEntity(formEntity);
		}

		return httpClient.execute(request);
	}
	
	/**
	 * 发送 put 请求。
	 * @param url 请求地址。
	 * @param headers 请求头参数集合。
	 * @param querys 地址栏请求参数集合。
	 * @param body 字符串类型请求体。
	 * @return 响应对象。
	 */
	public static HttpResponse doPut(String url, Map<String, String> headers, Map<String, String> querys, String body)
            throws Exception {
		CloseableHttpClient httpClient = wrapClient(url);
		HttpPut request = new HttpPut(buildUrl(url, querys));
		setConfig(request);
		setHeaders(request, headers);

		if (StringUtils.isNotBlank(body)) {
			request.setEntity(new StringEntity(body, CHARSET));
		}

		return httpClient.execute(request);
    }
	
	/**
	 * 发送 put 请求。
	 * <p>更新文件。
	 * @param url 请求地址。
	 * @param headers 请求头参数集合。
	 * @param querys 地址栏请求参数集合。
	 * @param body 文件类型请求体。
	 * @return 响应对象。
	 */
	public static HttpResponse doPut(String url, Map<String, String> headers, Map<String, String> querys, byte[] body)
			throws Exception {
		CloseableHttpClient httpClient = wrapClient(url);
		HttpPut request = new HttpPut(buildUrl(url, querys));
		setConfig(request);
		setHeaders(request, headers);

		if (ArrayUtils.isNotEmpty(body)) {
			request.setEntity(new ByteArrayEntity(body));
		}

		return httpClient.execute(request);
    }
	
	/**
	 * 发送 delete 请求。
	 * @param url 请求地址。
	 * @param headers 请求头参数集合。
	 * @param querys 地址栏请求参数集合。
	 * @return 响应对象。
	 */
	public static HttpResponse doDelete(String url, Map<String, String> headers, Map<String, String> querys)
			throws Exception {
		CloseableHttpClient httpClient = wrapClient(url);
		HttpDelete request = new HttpDelete(buildUrl(url, querys));
		setConfig(request);
		setHeaders(request, headers);

		return httpClient.execute(request);
    }
	
	/**
	 * 配置超时时间。
	 * @param connectTimeout 建立连接超时毫秒数。
	 * @param readTimeout 读取数据超时毫秒数。
	 */
	public static void configTimeout(int connectTimeout, int readTimeout) {
		CONNECT_TIMEOUT.set(connectTimeout);
		READ_TIMEOUT.set(readTimeout);
	}
	
	/**
	 * 获取下载文件名。
	 * @param response HTTP 响应对象。
	 * @return 文件名。
	 */
	public static String getFilename(HttpResponse response) throws UnsupportedEncodingException {
		Header contentDisposition = response.getFirstHeader("Content-Disposition");
		if (contentDisposition != null) {
			String filename = StringUtils.substringAfter(contentDisposition.getValue(), "filename=");
			filename = new String(filename.getBytes("ISO8859-1"), CHARSET);
			return filename;
		}
		return "";
	}

	
	/**
	 * 设置 http 客户端。
	 * @param url 请求地址。 请求地址。
	 * @return http 客户端。
	 */
	private static CloseableHttpClient wrapClient(String url) {
		HttpClientBuilder clientBuilder = HttpClientBuilder.create();
		if (url.startsWith("https://")) {
			clientBuilder
				.setSSLContext(createSSLContext())
				.setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE);
		}
		return clientBuilder.build();
	}
	
	/**
	 * 请求配置。
	 * @param request 请求对象。
	 */
	private static void setConfig(HttpRequestBase request) {
		Integer connectTimeout = CONNECT_TIMEOUT.get();
		Integer readTimeout = READ_TIMEOUT.get();
		
		RequestConfig config = RequestConfig.custom()
				.setConnectTimeout(connectTimeout != null ? connectTimeout : DEFAULT_CONNECT_TIMEOUT)	// 建立连接超时时间
				.setSocketTimeout(readTimeout != null ? readTimeout : DEFAULT_READ_TIMEOUT)	// 接收数据超时时间
				.build();
		request.setConfig(config);
	}
	
	/**
	 * 设置请求头信息。
	 * @param request 请求对象。
	 * @param headers 请求头参数集合。
	 */
	private static void setHeaders(HttpRequestBase request, Map<String, String> headers) {
		if (MapUtils.isNotEmpty(headers)) {
			headers.forEach(request::addHeader);
    	}
	}
	
	/**
	 * 创建 SSL 上下文。
	 * @return SSL 上下文对象。
	 */
	private static SSLContext createSSLContext() {
		SSLContext sslContext;
		try {
			sslContext = SSLContext.getInstance("TLS");
			X509TrustManager trustManager = new X509TrustManager() {
				public void checkClientTrusted(
						java.security.cert.X509Certificate[] paramArrayOfX509Certificate,
						String paramString) {
				}
	
				public void checkServerTrusted(
						java.security.cert.X509Certificate[] paramArrayOfX509Certificate,
						String paramString) {
				}
	
				public java.security.cert.X509Certificate[] getAcceptedIssuers() {
					return null;
				}
			};
			sslContext.init(null, new TrustManager[] { trustManager }, null);
		} catch (Exception e) {
            throw new RuntimeException(e);
        }
		return sslContext;
	}
	
	/**
	 * 把地址栏请求参数拼接到 url 中。
	 * @param url 请求地址。
	 * @param querys 地址栏请求参数集合。
	 * @return 附加地址栏请求参数的 url 。
	 */
	private static String buildUrl(String url, Map<String, String> querys) throws UnsupportedEncodingException {
    	StringBuilder sbUrl = new StringBuilder();
    	sbUrl.append(url.endsWith("/") ? url.substring(0, url.length() - 1) : url);
    	if (MapUtils.isNotEmpty(querys)) {
    		StringBuilder sbQuery = new StringBuilder();
        	for (Entry<String, String> query : querys.entrySet()) {
        		if (sbQuery.length() > 0) {
        			sbQuery.append("&");
        		}
        		if (StringUtils.isBlank(query.getKey()) && StringUtils.isNotBlank(query.getValue())) {
        			sbQuery.append(query.getValue());
                }
        		if (StringUtils.isNotBlank(query.getKey())) {
        			sbQuery.append(query.getKey());
        			if (StringUtils.isNotBlank(query.getValue())) {
        				sbQuery.append("=").append(URLEncoder.encode(query.getValue(), CHARSET));
        			}
                }
        	}
        	if (sbQuery.length() > 0) {
        		sbUrl.append(url.contains("?") ? "&" : "?").append(sbQuery);
        	}
        }
    	url = sbUrl.toString();
    	return url;
    }
	
	@Data
	@Accessors(chain = true)
	@NoArgsConstructor(force = true)
	@RequiredArgsConstructor
	@AllArgsConstructor
	public static class ByteArrayMultipartFile implements Serializable {

		private static final long serialVersionUID = 1774567846144961045L;

		private String name = "file";

		@NonNull
		private String originalFilename;

		@NonNull
		private byte[] bytes;
		
		private String contentType;
		
		/**
		 * 获取文件名扩展名，不含 "." 。
		 * @return 文件名扩展名，不含 "." 。
		 */
		@JsonIgnore
		public String getFilenameExtension() {
			return StringUtils.isNotBlank(originalFilename) && originalFilename.contains(".")
					? StringUtils.substringAfterLast(originalFilename, ".").toLowerCase()
					: "";
		}

	}
	
}
